package ecpay

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"reflect"
	"sort"
	"strings"
)

const (
	defaultBaseURL = "https://developer.toutiao.com/api/apps/ecpay/"
	userAgent      = "go-ecpay"
)

// Client manages communication with the ecpay API.
type Client struct {
	client *http.Client
	// Base URL for API.requests.
	// BaseURL should always be specified with a trailing slash.
	BaseURL *url.URL
	// User agent used when communication with bytedance API.
	UserAgent string

	appSecret    string
	thirdPartyID string

}


// Sign 签名
func (c *Client) Sign(v interface{}) string {
	var buf strings.Builder
	el := reflect.ValueOf(v)
	if el.Kind() == reflect.Ptr {
		el = el.Elem()
	}
	iVal := el
	typ := iVal.Type()
	var vv []string
	vv = append(vv, c.appSecret)
	for i := 0; i < iVal.NumField(); i++ {
		if iVal.Field(i).IsZero() {
			continue
		}
		fi := typ.Field(i)
		tag := fi.Tag.Get("json")
		if tag == "-" {
			continue
		}
		name := strings.Split(tag, ",")[0]
		if name == "" {
			name = fi.Name
		}
		if name == "Sign" || name =="access_token"|| name == "app_id"|| name == "thirdparty_id" {
			continue
		}
		vv = append(vv, fmt.Sprint(iVal.Field(i)))
	}

	sort.Strings(vv)
	for _, v := range vv {

		if v == "" {
			continue
		}
		if buf.Len() > 0 {
			buf.WriteByte('&')
		}
		buf.WriteString(v)
	}
	valuesStr := buf.String()
	h := md5.New()
	_, _ = io.WriteString(h, valuesStr)
	return fmt.Sprintf("%x", h.Sum(nil))
}

// Option 参数配置方法
type Option func(*Client)
// Options 公共请求参数

type Options struct {
	Client      *http.Client // 请求的客户端
}


// WithClient 注入自定义http客户端
func WithClient(client *http.Client) Option {
	return func(c *Client) {
		c.client = client
	}
}

// WithThirdPartyID 制定第三方ID
func WithThirdPartyID(thirdPartyID string) Option {
	return func(c *Client) {
		c.thirdPartyID = thirdPartyID
	}
}

// NewClient returns a new bytedance API client.
func NewClient(thirdPartyID, appSecret string, setters ...Option) *Client {
	baseURL, _ := url.Parse(defaultBaseURL)
	c := Client{
		client:       &http.Client{},
		BaseURL:      baseURL,
		UserAgent:    userAgent,
		appSecret:    appSecret,
		thirdPartyID: thirdPartyID,
	}
	for _, setter := range setters {
		setter(&c)
	}
	return &c
}

// NewRequest creates an API request. A relative URL can be provided in urlStr,
// in which case it is resolved relative to the BaseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
// If specified, the value pointed to by body is JSON encoded and included as
// request body.
func (c *Client) NewRequest(method, urlStr string, body body) (*http.Request, error) {
	u, err := c.BaseURL.Parse(urlStr)
	if err != nil {
		return nil, err
	}
	var buf bytes.Buffer
	if body != nil {
		body.SetSign(c.Sign(body))
		body.SetThirdPartyID(c.thirdPartyID)
		enc := json.NewEncoder(&buf)
		enc.SetEscapeHTML(false)
		if err := enc.Encode(body); err != nil {
			return nil, err
		}
	}

	req, err := http.NewRequest(method, u.String(), &buf)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	if c.UserAgent != "" {
		req.Header.Set("User-Agent", c.UserAgent)
	}
	return req, nil
}

// ErrorResponse represents bytedance ErrorResponse.
type ErrorResponse struct {
	ErrNo   int    `json:"err_no"`
	ErrTips string `json:"err_tips"`
}

// Error implements builtin.error interface.
func (r *ErrorResponse) Error() string {
	return fmt.Sprintf(" %d-%v", r.ErrNo, r.ErrTips)
}

func parseResponse(r *http.Response, v interface{}) error {
	var base ErrorResponse
	data, err := ioutil.ReadAll(r.Body)
	if err == nil && data != nil {
		if err = json.Unmarshal(data, &base); err != nil {
			return fmt.Errorf("unmarshal base repsonse err")
		}
	}
	r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
	if base.ErrNo == 0 {
		if err = json.Unmarshal(data, v); err != nil {
			return fmt.Errorf("unmarshal biz repsonse err")
		}
		return nil
	} else {
		return &base
	}
}


// Do Sends an API request and returns the API response. The API response is
// JSON decoded and stored in the value pointed to by v, or returned as an
// error if an API error has occurred. If v implements the io.Writer interface,
// the raw response body will be written to v, without attempting to first
// decode it.
//
// The provided ctx must be non-nil, if it is nil an error is returned. If it
// is canceled or timeout, ctx.Err() will be returned.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) error {
	if ctx == nil {
		return  errors.New("context must be non-nil")
	}

	req = req.WithContext(ctx)

	// save body for display request error info.
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return  errors.New("read request body error")
	}

	req.Body = ioutil.NopCloser(bytes.NewBuffer(body))

	resp, err := c.client.Do(req)
	if err != nil {
		// If we got an error, and the context has been canceled,
		// the context's error id probably more useful.
		select {
		case <-ctx.Done():
			return  ctx.Err()
		default:
		}
		return  err
	}

	err = parseResponse(resp, v)
	if err != nil {
		return  err
	}
	return  err
}
type body interface {
	SetSign(sign string)
	SetThirdPartyID(sign string)
}

type baseBody struct {
	Sign         string `json:"Sign,omitempty"`
	ThirdPartyID string `json:"thirdparty_id,omitempty"`
}

// SetSign 设置签名
func (b *baseBody) SetSign(sign string) {
	b.Sign = sign
}

// SetThirdPartyID 设置第三方id
func (b *baseBody) SetThirdPartyID(thirdPartyID string) {
	b.ThirdPartyID = thirdPartyID
}